// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Framework; // ITaskItem
using Microsoft.Build.Utilities; // Task

namespace ILLink.Tasks
{
    public class CreateRuntimeRootILLinkDescriptorFile : Task
    {
        /// <summary>
        ///   The path to namespace.h.
        /// </summary>
        [Required]
        public ITaskItem NamespaceFilePath { get; set; }

        /// <summary>
        ///   The path to mscorlib.h.
        /// </summary>
        [Required]
        public ITaskItem MscorlibFilePath { get; set; }

        /// <summary>
        ///   The path to cortypeinfo.h.
        /// </summary>
        [Required]
        public ITaskItem CortypeFilePath { get; set; }

        /// <summary>
        ///   The path to rexcep.h.
        /// </summary>
        [Required]
        public ITaskItem RexcepFilePath { get; set; }

        /// <summary>
        ///   The path to ILLinkTrim.xml.
        /// </summary>
        [Required]
        public ITaskItem ILLinkTrimXmlFilePath { get; set; }

        public ITaskItem[] DefineConstants { get; set; }

        /// <summary>
        ///   The path to the file to generate.
        /// </summary>
        [Required]
        public ITaskItem RuntimeRootDescriptorFilePath { get; set; }

        sealed class ClassMembers
        {
            public bool keepAllFields;
            public HashSet<string> methods;
            public HashSet<string> fields;
        }

        /// <summary>
        /// Helper utility to track feature switch macros in header file
        /// This type is used in a dictionary as a key
        /// </summary>
        readonly struct FeatureSwitchMembers : IEquatable<FeatureSwitchMembers>
        {
            public string Feature { get; }
            public string FeatureValue { get; }
            public string FeatureDefault { get; }
            // Unique value to track the key
            private readonly string _key;

            public FeatureSwitchMembers(string feature, string featureValue, string featureDefault)
            {
                Feature = feature;
                FeatureValue = featureValue;
                FeatureDefault = featureDefault;
                // Use a separator that is not going to be in any of the strings to ensure uniqueness
                _key = feature + ',' + featureValue + ',' + featureDefault;
            }

            public override int GetHashCode()
            {
                return _key.GetHashCode();
            }

            public override bool Equals(object obj)
                => obj is FeatureSwitchMembers inst && Equals(inst);

            public bool Equals(FeatureSwitchMembers fsm)
                => fsm._key == _key;
        }

        readonly Dictionary<string, string> namespaceDictionary = new Dictionary<string, string>();
        readonly Dictionary<string, string> classIdsToClassNames = new Dictionary<string, string>();
        readonly Dictionary<string, ClassMembers> classNamesToClassMembers = new Dictionary<string, ClassMembers>();
        readonly Dictionary<FeatureSwitchMembers, Dictionary<string, ClassMembers>> featureSwitchMembers = new();
        readonly HashSet<string> defineConstants = new HashSet<string>(StringComparer.Ordinal);

        public override bool Execute()
        {
            var namespaceFilePath = NamespaceFilePath.ItemSpec;
            if (!File.Exists(namespaceFilePath))
            {
                Log.LogError($"File '{namespaceFilePath}' doesn't exist.");
                return false;
            }

            var mscorlibFilePath = MscorlibFilePath.ItemSpec;
            if (!File.Exists(mscorlibFilePath))
            {
                Log.LogError($"File '{mscorlibFilePath}' doesn't exist.");
                return false;
            }

            var cortypeFilePath = CortypeFilePath.ItemSpec;
            if (!File.Exists(cortypeFilePath))
            {
                Log.LogError($"File '{cortypeFilePath}' doesn't exist.");
                return false;
            }

            var rexcepFilePath = RexcepFilePath.ItemSpec;
            if (!File.Exists(rexcepFilePath))
            {
                Log.LogError($"File '{rexcepFilePath}' doesn't exist.");
                return false;
            }

            InitializeDefineConstants();

            var iLLinkTrimXmlFilePath = ILLinkTrimXmlFilePath.ItemSpec;
            if (!File.Exists(iLLinkTrimXmlFilePath))
            {
                Log.LogError($"File '{iLLinkTrimXmlFilePath}' doesn't exist.");
                return false;
            }

            ProcessNamespaces(namespaceFilePath);

            ProcessMscorlib(mscorlibFilePath);

            ProcessCoreTypes(cortypeFilePath);

            ProcessExceptionTypes(rexcepFilePath);

            OutputXml(iLLinkTrimXmlFilePath, RuntimeRootDescriptorFilePath.ItemSpec);

            return true;
        }

        void ProcessNamespaces(string namespaceFile)
        {
            string[] namespaces = File.ReadAllLines(namespaceFile);

            // Process definitions of the form
            // #define g_SystemNS          "System"
            // from namespace.h
            foreach (string namespaceDef in namespaces)
            {
                if (namespaceDef.StartsWith("#define"))
                {
                    char[] separators = { '"', ' ', '\t' };
                    string[] namespaceDefElements = namespaceDef.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                    int startIndex = "g_".Length;
                    // E.g., if namespaceDefElements [1] is "g_RuntimeNS", lhs is "Runtime".
                    string lhs = namespaceDefElements[1].Substring(startIndex, namespaceDefElements[1].LastIndexOf('N') - startIndex);
                    if (namespaceDefElements.Length == 3)
                    {
                        // E.G., #define g_SystemNS          "System"
                        // "System" --> "System"
                        namespaceDictionary[lhs] = namespaceDefElements[2];
                    }
                    else
                    {
                        // E.g., #define g_RuntimeNS         g_SystemNS ".Runtime"
                        // "Runtime" --> "System.Runtime"
                        string prefix = namespaceDefElements[2].Substring(startIndex, namespaceDefElements[2].LastIndexOf('N') - startIndex);
                        namespaceDictionary[lhs] = namespaceDictionary[prefix] + namespaceDefElements[3];
                    }
                }
            }
        }

        void ProcessMscorlib(string typeFile)
        {
            string[] types = File.ReadAllLines(typeFile);
            DefineTracker defineTracker = new DefineTracker(defineConstants, Log, typeFile);
            string classId;
            FeatureSwitchMembers? currentFeatureSwitch = null;

            foreach (string def in types)
            {
                if (defineTracker.ProcessLine(def) || !defineTracker.IsActiveSection)
                    continue;

                //We will handle BEGIN_ILLINK_FEATURE_SWITCH and END_ILLINK_FEATURE_SWITCH
                if (def.StartsWith("BEGIN_ILLINK_FEATURE_SWITCH"))
                {
                    if (currentFeatureSwitch.HasValue)
                    {
                        Log.LogError($"Could not figure out feature switch status in '{typeFile}' for line {def}.");
                    }
                    else
                    {
                        char[] separators = { ',', '(', ')', ' ', '\t', '/' };
                        string[] featureSwitchElements = def.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                        if (featureSwitchElements.Length != 4)
                        {
                            Log.LogError($"BEGIN_ILLINK_FEATURE_SWITCH is not formatted correctly '{typeFile}' for line {def}.");
                            return;
                        }
                        currentFeatureSwitch = new FeatureSwitchMembers(featureSwitchElements[1], featureSwitchElements[2], featureSwitchElements[3]);
                        if (!featureSwitchMembers.ContainsKey(currentFeatureSwitch.Value))
                        {
                            featureSwitchMembers.Add(currentFeatureSwitch.Value, new Dictionary<string, ClassMembers>());
                        }
                    }
                }
                if (def.StartsWith("END_ILLINK_FEATURE_SWITCH"))
                {
                    if (!currentFeatureSwitch.HasValue)
                    {
                        Log.LogError($"Could not figure out feature switch status in '{typeFile}' for line {def}.");
                    }
                    else
                    {
                        currentFeatureSwitch = null;
                    }
                }

                string[] defElements = null;
                if (def.StartsWith("DEFINE_") || def.StartsWith("// DEFINE_"))
                {
                    char[] separators = { ',', '(', ')', ' ', '\t', '/' };
                    defElements = def.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                }

                if (def.StartsWith("DEFINE_CLASS(") || def.StartsWith("// DEFINE_CLASS("))
                {
                    // E.g., DEFINE_CLASS(APP_DOMAIN,            System,                 AppDomain)
                    classId = defElements[1];               // APP_DOMAIN
                    string classNamespace = defElements[2]; // System
                    string className = defElements[3];      // AppDomain
                    AddClass(classNamespace, className, classId, false, currentFeatureSwitch);
                }
                else if (def.StartsWith("DEFINE_CLASS_U("))
                {
                    // E.g., DEFINE_CLASS_U(System,                 AppDomain,      AppDomainBaseObject)
                    string classNamespace = defElements[1]; // System
                    string className = defElements[2];      // AppDomain
                    classId = defElements[3];               // AppDomainBaseObject
                                                            // For these classes the sizes of managed and unmanaged classes and field offsets
                                                            // are compared so we need to preserve all fields.
                    const bool keepAllFields = true;
                    AddClass(classNamespace, className, classId, keepAllFields, currentFeatureSwitch);
                }
                else if (def.StartsWith("DEFINE_FIELD("))
                {
                    // E.g., DEFINE_FIELD(ACCESS_VIOLATION_EXCEPTION, IP,                _ip)
                    classId = defElements[1];          // ACCESS_VIOLATION_EXCEPTION
                    string fieldName = defElements[3]; // _ip
                    AddField(fieldName, classId, currentFeatureSwitch);
                }
                else if (def.StartsWith("DEFINE_METHOD("))
                {
                    // E.g., DEFINE_METHOD(APP_DOMAIN,           ON_ASSEMBLY_LOAD,       OnAssemblyLoadEvent,        IM_Assembly_RetVoid)
                    string methodName = defElements[3]; // OnAssemblyLoadEvent
                    classId = defElements[1];           // APP_DOMAIN
                    AddMethod(methodName, classId, null, null, currentFeatureSwitch);
                }
                else if (def.StartsWith("DEFINE_PROPERTY(") || def.StartsWith("DEFINE_STATIC_PROPERTY("))
                {
                    // E.g., DEFINE_PROPERTY(ARRAY,              LENGTH,                 Length,                     Int)
                    // or    DEFINE_STATIC_PROPERTY(THREAD,      CURRENT_THREAD,         CurrentThread,              Thread)
                    string propertyName = defElements[3];          // Length or CurrentThread
                    classId = defElements[1];                      // ARRAY or THREAD
                    AddMethod("get_" + propertyName, classId, null, null, currentFeatureSwitch);
                }
                else if (def.StartsWith("DEFINE_SET_PROPERTY("))
                {
                    // E.g., DEFINE_SET_PROPERTY(THREAD,         UI_CULTURE,             CurrentUICulture,           CultureInfo)
                    string propertyName = defElements[3]; // CurrentUICulture
                    classId = defElements[1];             // THREAD
                    AddMethod("get_" + propertyName, classId, null, null, currentFeatureSwitch);
                    AddMethod("set_" + propertyName, classId, null, null, currentFeatureSwitch);
                }
            }
        }

        public void ProcessCoreTypes(string corTypeFile)
        {
            string[] corTypes = File.ReadAllLines(corTypeFile);
            DefineTracker defineTracker = new DefineTracker(defineConstants, Log, corTypeFile);

            foreach (string def in corTypes)
            {
                if (defineTracker.ProcessLine(def) || !defineTracker.IsActiveSection)
                    continue;

                // E.g., TYPEINFO(ELEMENT_TYPE_VOID,         "System", "Void",          0,              TYPE_GC_NONE,   false,  true,   false,  false,  false) // 0x01
                if (def.StartsWith("TYPEINFO("))
                {
                    char[] separators = { ',', '(', ')', '"', ' ', '\t' };
                    string[] defElements = def.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                    string classId = null;
                    string classNamespace = defElements[2]; // System
                    string className = defElements[3];      // Void
                    AddClass(classNamespace, className, classId);
                }
            }
        }

        public void ProcessExceptionTypes(string excTypeFile)
        {
            string[] excTypes = File.ReadAllLines(excTypeFile);
            DefineTracker defineTracker = new DefineTracker(defineConstants, Log, excTypeFile);

            foreach (string def in excTypes)
            {
                if (defineTracker.ProcessLine(def) || !defineTracker.IsActiveSection)
                    continue;

                // E.g., DEFINE_EXCEPTION(g_InteropNS,          MarshalDirectiveException,      false,  COR_E_MARSHALDIRECTIVE)
                if (def.StartsWith("DEFINE_EXCEPTION("))
                {
                    char[] separators = { ',', '(', ')', ' ', '\t' };
                    string[] defElements = def.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                    string classId = null;
                    string classNamespace = defElements[1]; // g_InteropNS
                    string className = defElements[2];      // MarshalDirectiveException
                    AddClass(classNamespace, className, classId);
                    AddMethod(".ctor", classId, classNamespace, className);
                }
            }
        }

        void OutputXml(string iLLinkTrimXmlFilePath, string outputFileName)
        {
            XmlDocument doc = new XmlDocument();
            using (var sr = new StreamReader(iLLinkTrimXmlFilePath))
            {
                using XmlReader reader = XmlReader.Create(sr, new XmlReaderSettings() { XmlResolver = null });
                doc.Load(reader);
            }

            XmlElement linkerNode = doc["linker"];

            if (featureSwitchMembers.Count > 0)
            {
                foreach ((var fs, var members) in featureSwitchMembers.Select(kv => (kv.Key, kv.Value)))
                {
                    if (members.Count == 0)
                        continue;

                    // <assembly fullname="System.Private.CoreLib" feature="System.Diagnostics.Tracing.EventSource.IsSupported" featurevalue="true" featuredefault="true">
                    XmlElement featureAssemblyNode = doc.CreateElement("assembly");
                    XmlAttribute featureAssemblyFullName = doc.CreateAttribute("fullname");
                    featureAssemblyFullName.Value = "System.Private.CoreLib";
                    featureAssemblyNode.Attributes.Append(featureAssemblyFullName);

                    XmlAttribute featureName = doc.CreateAttribute("feature");
                    featureName.Value = fs.Feature;
                    featureAssemblyNode.Attributes.Append(featureName);

                    XmlAttribute featureValue = doc.CreateAttribute("featurevalue");
                    featureValue.Value = fs.FeatureValue;
                    featureAssemblyNode.Attributes.Append(featureValue);

                    XmlAttribute featureDefault = doc.CreateAttribute("featuredefault");
                    featureDefault.Value = fs.FeatureDefault;
                    featureAssemblyNode.Attributes.Append(featureDefault);

                    foreach (var type in members)
                    {
                        AddXmlTypeNode(doc, featureAssemblyNode, type.Key, type.Value);
                    }
                    linkerNode.AppendChild(featureAssemblyNode);
                }
            }

            XmlNode assemblyNode = linkerNode["assembly"];

            foreach (var type in classNamesToClassMembers)
            {
                AddXmlTypeNode(doc, assemblyNode, type.Key, type.Value);
            }
            doc.Save(outputFileName);
        }

        static void AddXmlTypeNode(XmlDocument doc, XmlNode assemblyNode, string typeName, ClassMembers members)
        {
            XmlElement typeNode = doc.CreateElement("type");
            XmlAttribute typeFullName = doc.CreateAttribute("fullname");
            typeFullName.Value = typeName;
            typeNode.Attributes.Append(typeFullName);

            // We need to keep everyting in System.Runtime.InteropServices.WindowsRuntime and
            // System.Threading.Volatile.
            if (!typeName.StartsWith("System.Runtime.InteropServices.WindowsRuntime") &&
                !typeName.StartsWith("System.Threading.Volatile"))
            {
                if (members.keepAllFields)
                {
                    XmlAttribute preserve = doc.CreateAttribute("preserve");
                    preserve.Value = "fields";
                    typeNode.Attributes.Append(preserve);
                }
                else if ((members.fields == null) && (members.methods == null))
                {
                    XmlAttribute preserve = doc.CreateAttribute("preserve");
                    preserve.Value = "nothing";
                    typeNode.Attributes.Append(preserve);
                }

                if (!members.keepAllFields && (members.fields != null))
                {
                    foreach (string field in members.fields)
                    {
                        XmlElement fieldNode = doc.CreateElement("field");
                        XmlAttribute fieldName = doc.CreateAttribute("name");
                        fieldName.Value = field;
                        fieldNode.Attributes.Append(fieldName);
                        typeNode.AppendChild(fieldNode);
                    }
                }

                if (members.methods != null)
                {
                    foreach (string method in members.methods)
                    {
                        XmlElement methodNode = doc.CreateElement("method");
                        XmlAttribute methodName = doc.CreateAttribute("name");
                        methodName.Value = method;
                        methodNode.Attributes.Append(methodName);
                        typeNode.AppendChild(methodNode);
                    }
                }
            }
            assemblyNode.AppendChild(typeNode);
        }


        void AddClass(string classNamespace, string className, string classId, bool keepAllFields = false, FeatureSwitchMembers? featureSwitch = null)
        {
            string fullClassName = GetFullClassName(classNamespace, className);
            if (fullClassName != null)
            {
                if ((classId != null) && (classId != "NoClass"))
                {
                    classIdsToClassNames[classId] = fullClassName;
                }

                ClassMembers members;
                if (!featureSwitch.HasValue)
                {
                    if (!classNamesToClassMembers.TryGetValue(fullClassName, out members))
                    {
                        members = new ClassMembers();
                        classNamesToClassMembers[fullClassName] = members;
                    }
                }
                else
                {
                    Dictionary<string, ClassMembers> currentFeatureSwitchMembers = featureSwitchMembers[featureSwitch.Value];
                    if (!currentFeatureSwitchMembers.TryGetValue(fullClassName, out members))
                    {
                        members = new ClassMembers();
                        currentFeatureSwitchMembers[fullClassName] = members;
                    }
                }
                members.keepAllFields |= keepAllFields;
            }
        }

        void AddField(string fieldName, string classId, FeatureSwitchMembers? featureSwitch = null)
        {
            string className = classIdsToClassNames[classId];
            ClassMembers members;

            if (!featureSwitch.HasValue)
            {
                members = classNamesToClassMembers[className];
            }
            else
            {
                members = featureSwitchMembers[featureSwitch.Value][className];
            }

            members.fields ??= new HashSet<string>();
            members.fields.Add(fieldName);
        }

        void AddMethod(string methodName, string classId, string classNamespace = null, string className = null, FeatureSwitchMembers? featureSwitch = null)
        {
            string fullClassName;
            if (classId != null)
            {
                fullClassName = classIdsToClassNames[classId];
            }
            else
            {
                fullClassName = GetFullClassName(classNamespace, className);
            }

            ClassMembers members;

            if (!featureSwitch.HasValue)
            {
                members = classNamesToClassMembers[fullClassName];
            }
            else
            {
                members = featureSwitchMembers[featureSwitch.Value][fullClassName];
            }

            members.methods ??= new HashSet<string>();
            members.methods.Add(methodName);
        }

        string GetFullClassName(string classNamespace, string className)
        {
            string prefixToRemove = "g_";
            if (classNamespace.StartsWith(prefixToRemove))
            {
                classNamespace = classNamespace.Substring(prefixToRemove.Length);
            }
            string suffixToRemove = "NS";
            if (classNamespace.EndsWith(suffixToRemove))
            {
                classNamespace = classNamespace.Substring(0, classNamespace.Length - suffixToRemove.Length);
            }

            if ((classNamespace == "NULL") && (className == "NULL"))
            {
                return null;
            }

            if (!namespaceDictionary.ContainsKey(classNamespace))
            {
                Log.LogError($"Unknown namespace '{classNamespace}'.");
            }

            // Convert from the System.Reflection/CoreCLR nested type name format to the IL/Cecil format.
            string classNameWithCecilNestedFormat = className.Replace('+', '/');

            return namespaceDictionary[classNamespace] + "." + classNameWithCecilNestedFormat;
        }

        void InitializeDefineConstants()
        {
            if (DefineConstants is null)
                return;

            foreach (var item in DefineConstants)
                defineConstants.Add(item.ItemSpec.Trim());
        }

        sealed class DefineTracker
        {
            readonly HashSet<string> defineConstants;
            readonly TaskLoggingHelper log;
            readonly string filePath;
            readonly Stack<bool?> activeSections = new Stack<bool?>();
            int linenum;

            public DefineTracker(HashSet<string> defineConstants, TaskLoggingHelper log, string filePath)
            {
                this.defineConstants = defineConstants;
                this.log = log;
                this.filePath = filePath;
            }

            public bool ProcessLine(string line)
            {
                linenum++;

                int ifdefLength = 0;
                bool negativeIfDef = false;
                if (line.StartsWith("#ifdef "))
                {
                    ifdefLength = 7;
                }
                else if (line.StartsWith("#ifndef "))
                {
                    ifdefLength = 8;
                    negativeIfDef = true;
                }
                else if (line.StartsWith("#if "))
                {
                    ifdefLength = 4;
                }

                if (ifdefLength > 0)
                {
                    if (activeSections.Count > 0 && activeSections.Peek() != true)
                    {
                        activeSections.Push(null);
                    }
                    else
                    {
                        string defineName = line.Substring(ifdefLength);
                        int commentIndex = defineName.IndexOf('/');
                        if (commentIndex >= 0)
                            defineName = defineName.Substring(0, commentIndex);

                        defineName = defineName.Trim();
                        if (defineConstants.Contains(defineName))
                            activeSections.Push(!negativeIfDef);
                        else
                            activeSections.Push(negativeIfDef);
                    }

                    return true;
                }

                if (line.StartsWith("#else"))
                {
                    if (activeSections.Count == 0)
                    {
                        log.LogError($"Could not figure out ifdefs in '{filePath}' around line {linenum}.");
                    }
                    else
                    {
                        bool? activeSection = activeSections.Pop();
                        if (activeSection == null)
                            activeSections.Push(null);
                        else
                            activeSections.Push(!activeSection.Value);
                    }

                    return true;
                }

                if (line.StartsWith("#endif"))
                {
                    if (activeSections.Count == 0)
                        log.LogError($"Could not figure out ifdefs in '{filePath}' around line {linenum}.");
                    else
                        activeSections.Pop();

                    return true;
                }

                return false;
            }

            public bool IsActiveSection
            {
                get => activeSections.Count == 0 || activeSections.Peek() == true;
            }
        }
    }
}
